/*
 * Copyright (c) 2007 Aleksey Nikiforov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'Aleksey Nikiforov' nor the names of other contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.lex.input.mouse.component;


import java.util.logging.Level;
import java.util.logging.Logger;

import org.lex.input.mouse.Cursor;

import com.jme.input.InputHandler;
import com.jme.math.Vector2f;
import com.jme.scene.Spatial;
import com.jme.system.DisplaySystem;

/**
 * This mouse implementation has a preferece setting to use harware mouse
 * whenever possible. It hides what implementation is used behind the scenes.
 * Methods that change mouse status [enable(), disable(), setCursorVisible(),
 * setActive(), setCursor(), setNativeMousePreferred()] are not guaranteed
 * to succeed.
 * If an error occurs and the system is unable to show the native
 * mouse, then software mouse is used instead. If an error prevents hiding
 * the native cursor, then software mouse will not engage.
 * 
 * @author lex
 *
 */
public class CombinedMouse implements AdvancedMouse, SelectableNativeMouse {

	private static final Logger log = Logger.getLogger(
			CombinedMouse.class.getName());
	
	private static final CombinedMouse mouse = new CombinedMouse();
	public static CombinedMouse get() { return mouse; }
	
	
	private HardwareMouse nativeMouse;
	private SoftwareMouse softMouse;
	
	private boolean preferNative;
	private boolean visible;
	private boolean active;
	
	private BasicMouse currentMouse;
	private boolean emulatingActive;
	private boolean emulatingVisible;
	private float softMouseSensitivity;
	
	/**
	 * This constuctor will create CombinedMouse in disabled state, unless
	 * the nativeMouse is enabled and cannot be deactivated.
	 */
	private CombinedMouse() {
		softMouse = new SoftwareMouse("Software Mouse",
				DisplaySystem.getDisplaySystem().getRenderer());
		nativeMouse = HardwareMouse.get();
		
		nativeMouse.disable();
		if (nativeMouse.isEnabled()) {
			currentMouse = nativeMouse;
			preferNative = true;
		} else {
			currentMouse = softMouse;
		}
		
		softMouseSensitivity = softMouse.getSensitivity();
	}
	
	/**
	 * Call this after changing display settings (needs more testing).
	 */
	public void reinit(int width, int height) {
		softMouse.reinit(width, height);
	}
	
	/**
	 * This method is mainly for testing/debugging.
	 * @return true, if software mouse component is visible, false otherwise
	 */
	public boolean isSoftwareMouseVisible() {
		return softMouse.isCursorVisible();
	}
	
	/**
	 * This method is mainly for testing/debugging.
	 * @return true, if software mouse component is active, false otherwise
	 */
	public boolean isSoftwareMouseActive() {
		return softMouse.isActive();
	}
	
	/**
	 * This method is mainly for testing/debugging.
	 * @return true, if hardware mouse component is enabled, false otherwise
	 */
	public boolean isNativeMouseEnabled() {
		return nativeMouse.isEnabled();
	}
	
	public boolean isActive() {
		return (nativeMouse.isEnabled() || softMouse.isActive());
	}

	public boolean isCursorVisible() {
		return (nativeMouse.isEnabled() || softMouse.isCursorVisible());
	}
	
	public boolean isEnabled() {
		return (nativeMouse.isEnabled() || softMouse.isEnabled());
	}
	
	private void syncState() {
		if (preferNative) {
			
			currentMouse = nativeMouse;
			
			if (!nativeMouse.isEnabled()) {
				
				if (softMouse.isActive()) {
					emulatingActive = true;
					active = true;
					currentMouse = softMouse;
				}
				if (softMouse.isCursorVisible()) {
					emulatingVisible = true;
					visible = true;
					currentMouse = softMouse;
				}
				
			} else {
				active = true;
				visible = true;
				emulatingActive = false;
				emulatingVisible = false;
			}
		} else {
			if (nativeMouse.isEnabled()) {
				log.log(Level.WARNING,
						"Native mouse is turned on by an external source.");
			}
			visible = softMouse.isCursorVisible();
			active = softMouse.isActive();
			currentMouse = softMouse;
		}
	}
	
	/**
	 * The mouse spatial for the software mouse. This spatial should be added
	 * to a node (e.g. rootNode) for rendering.
	 * @return the mouse spatial that should be added to a node for rendering.
	 */
	public Spatial getMouseSpatial() {
		return softMouse.getMouseSpatial();
	}
	
	/**
	 * Senstivity of the software mouse.
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.AdvancedMouse#getSensitivity()
	 */
	public float getSensitivity() {
		return softMouseSensitivity;
	}
	
	/**
	 * Only applies to software mouse.
	 * This is the recommended way of setting sensitivty. The software mouse
	 * is being used instead of native mouse when using active-invisible mode,
	 * during that time the sensitivity is overwritten. Setting the sensitivity
	 * by means other than this method might result in loosing changes in
	 * some cases.
	 * @param sensitivity
	 */
	public void setSensitivity(float sensitivity) {
		softMouseSensitivity = sensitivity;
		if (!emulatingActive) softMouse.setSensitivity(sensitivity);
	}
	
	private void switchMouse() {
		if (currentMouse == nativeMouse) {
			softMouse.setHotSpotPosition(nativeMouse.getHotSpotPosition());
			currentMouse = softMouse;
		} else {
			nativeMouse.setHotSpotPosition(softMouse.getHotSpotPosition());
			currentMouse = nativeMouse;
		}
	}
	
	private void useBackup() {
		softMouse.enable();
		currentMouse = softMouse;
		
		visible = true;
		active = true;
		
		emulatingActive = false;
		emulatingVisible = false;
		
		log.log(Level.WARNING,
				"Hardware mouse error, software mouse is used as a backup.");
	}
	
	private void disableNativeError() {
		log.log(Level.WARNING,
			"Unable to turn off harware mouse, software mouse is not engaged.");
	}
	

	// *************************************************************************
	
	public void setActive(boolean active) {
		syncState();
		if (active == this.active) return;
		
		if (preferNative) {
			
			if (active) {
				
				if (emulatingVisible) {
					switchMouse();
					nativeMouse.enable();
					if (nativeMouse.isEnabled()) {
						softMouse.disable();
						emulatingVisible = false;
					} else {
						useBackup();						
					}
				} else {
					nativeMouse.disable();
					if (!nativeMouse.isEnabled()) {
						switchMouse();
						softMouse.disable();
						softMouse.setActive(true);
						softMouse.setSensitivity(1);
						emulatingActive = true;
					} else {
						disableNativeError();
						return;
					}
				}
				
			} else {
				
				if (emulatingActive) {
					softMouse.disable();
					softMouse.setSensitivity(softMouseSensitivity);
					switchMouse();
					emulatingActive = false;
				} else {
					nativeMouse.disable();
					if (!nativeMouse.isEnabled()) {
						switchMouse();
						softMouse.disable();
						softMouse.setCursorVisible(true);
						emulatingVisible = true;
					} else {
						disableNativeError();
						return;
					}
				}
				
			}
			
		} else {
			softMouse.setActive(active);
		}
		
		this.active = active;
	}

	public void setCursorVisible(boolean visible) {
		syncState();
		if (visible == this.visible) return;
		
		if (preferNative) {
			
			if (visible) {
				
				if (emulatingActive) {
					softMouse.setSensitivity(softMouseSensitivity);
					switchMouse();
					nativeMouse.enable();
					if (nativeMouse.isEnabled()) {
						softMouse.disable();
						emulatingActive = false;
					} else {
						useBackup();
					}
				} else {
					nativeMouse.disable();
					if (!nativeMouse.isEnabled()) {
						switchMouse();
						softMouse.disable();
						softMouse.setCursorVisible(true);
						emulatingVisible = true;
					} else {
						disableNativeError();
						return;
					}
				}
				
			} else {
				
				if (emulatingVisible) {
					softMouse.disable();
					switchMouse();
					emulatingVisible = false;
				} else {
					nativeMouse.disable();
					if (!nativeMouse.isEnabled()) {
						switchMouse();
						softMouse.disable();
						softMouse.setActive(true);
						softMouse.setSensitivity(1);
						emulatingActive = true;
					} else {
						disableNativeError();
						return;
					}
				}
				
			}
			
		} else {
			softMouse.setCursorVisible(visible);
		}
		
		this.visible = visible;
	}

	public void disable() {
		syncState();
		if (!visible && !active) return;
		
		if (preferNative) {
			
			if (emulatingVisible) {
				setCursorVisible(false);
			} else if (emulatingActive) {
				setActive(false);
			} else {
				nativeMouse.disable();
				if (nativeMouse.isEnabled()) {
					disableNativeError();
					return;
				}
			}
			
		} else {
			softMouse.disable();
		}
		
		visible = false;
		active = false;
	}

	public void enable() {
		syncState();
		if (visible && active) return;
		
		if (preferNative) {
			
			if (emulatingVisible) {
				setActive(true);
			} else if (emulatingActive) {
				setCursorVisible(true);
			} else {
				nativeMouse.enable();
				if (!nativeMouse.isEnabled()) useBackup();
			}
			
		} else {
			softMouse.enable();
		}
		
		visible = true;
		active = true;
	}
	
	/* (non-Javadoc)
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.SelectableNativeMouse#setNativeMousePreferred(boolean)
	 */
	public void setNativeMousePreferred(boolean useNative) {
		syncState();
		if (useNative == preferNative) return;
		
		if (useNative) {
			
			if (visible && !active) {
				emulatingVisible = true;
			} else if (active && !visible) {
				softMouse.setSensitivity(1);
				emulatingActive = true;
			} else if (active && visible){
				switchMouse();
				nativeMouse.enable();
				if (nativeMouse.isEnabled()) {
					softMouse.disable();
				} else {
					useBackup();
					return;
				}
			} else {
				switchMouse();
			}
			
		} else {
			
			if (emulatingVisible) {
				emulatingVisible = false;
			} else if (emulatingActive) {
				softMouse.setSensitivity(softMouseSensitivity);
				emulatingActive = false;
			} else if (visible && active) {
				nativeMouse.disable();
				if (!nativeMouse.isEnabled()) {
					switchMouse();
					softMouse.enable();
				} else {
					disableNativeError();
					return;
				}
			} else {
				switchMouse();
			}
			
		}
		
		preferNative = useNative;
	}

	public void setCursor(Cursor cursor) {
		syncState();
		boolean nativeWasOn = false;
		if (nativeMouse.isEnabled()) nativeWasOn = true; 
		
		nativeMouse.setCursor(cursor);
		softMouse.setCursor(cursor);
		
		if (nativeWasOn) {
			if (nativeMouse.isEnabled()) {
				softMouse.setHotSpotPosition(nativeMouse.getHotSpotPosition());
				softMouse.disable();
			} else {
				useBackup();
			}
		}
		
	}
	
	// *************************************************************************
	
	/* (non-Javadoc)
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.SelectableNativeMouse#isNativeMousePreferred()
	 */
	public boolean isNativeMousePreferred() {
		return preferNative;
	}

	public Cursor getCursor() {
		return currentMouse.getCursor();
	}
	
	public Vector2f getHotSpotPosition() {
		return currentMouse.getHotSpotPosition();
	}
	
	public void setHotSpotPosition(float x, float y) {
		currentMouse.setHotSpotPosition(x, y);
	}

	public void setHotSpotPosition(Vector2f position) {
		currentMouse.setHotSpotPosition(position);
	}

	public void registerWithInputHandler(InputHandler handler) {
		nativeMouse.registerWithInputHandler(handler);
		softMouse.registerWithInputHandler(handler);
	}

	public float getWidth() {
		return softMouse.getWidth();
	}
	
	public float getHeight() {
		return softMouse.getHeight();
	}

}
